package com.emeraldparser.symbol;

import static com.emeraldparser.util.Automatons.deterministic;
import static com.emeraldparser.util.Automatons.transformVertices;
import static com.emeraldparser.util.Functions2.memoized;

import java.util.Map;
import java.util.Set;

import com.emeraldparser.util.Automaton;
import com.emeraldparser.util.Automatons;
import com.emeraldparser.util.HashCodes;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

public final class ConjunctionSymbolClass extends AbstractSymbolClass {

	private final ImmutableSet<? extends SymbolClass> conjunctedSymbolClasses;
	private final transient int hashCode;

	public ConjunctionSymbolClass(SymbolClass... conjunctedSymbolClasses) {
		this(ImmutableSet.copyOf(conjunctedSymbolClasses));
	}

	public ConjunctionSymbolClass(Iterable<? extends SymbolClass> conjunctedSymbolClasses) {
		this(ImmutableSet.copyOf(conjunctedSymbolClasses));
	}

	public ConjunctionSymbolClass(ImmutableSet<? extends SymbolClass> conjunctedSymbolClasses) {
		this.conjunctedSymbolClasses = Preconditions.checkNotNull(conjunctedSymbolClasses, "Conjuncted symbol classes must not be null");
		this.hashCode = HashCodes.classSpecificHashCode(ConjunctionSymbolClass.class, conjunctedSymbolClasses);
	}

	public ImmutableSet<? extends SymbolClass> getConjunctedSymbolClasses() {
		return conjunctedSymbolClasses;
	}

	@Override
	public String toString() {
		return "(" + Joiner.on(" & ").join(conjunctedSymbolClasses) + ")";
	}

	@Override
	public Automaton<? extends State, ? extends SymbolClass> toAutomaton() {
		// TODO Auto-generated method stub

		Automaton<State, SymbolClass> nfa = Automatons.create(super.toAutomaton());
		
		Map<SymbolClass, Automaton<? extends State, ? extends SymbolClass>> automatons = Maps.newHashMap();
		for (SymbolClass conjunctedSymbolClass : conjunctedSymbolClasses) {
			Automaton<? extends State, ? extends SymbolClass> automaton = conjunctedSymbolClass.toAutomaton();
			automatons.put(conjunctedSymbolClass, automaton);
		}
		
		Automaton<State, SymbolClass> dfa = transformVertices(
				deterministic(nfa),
				memoized(
					new Function<ImmutableSet<State>, State>() {
						@Override
						public State apply(ImmutableSet<State> conjunctedStates) {
							return new ConjunctionState(conjunctedStates);
						}
					}
				)
			);
		
		Set<ImmutableSet<State>> acceptDfaStates = Sets.newHashSet();
		
		for (Automaton<? extends State, ? extends SymbolClass> automaton : automatons.values()) {
			automaton.getAcceptVertices();
		}
		
		return nfa;
	}

	@Override
	public int hashCode() {
		return hashCode;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		
		if (obj == null) {
			return false;
		}
		
		if (getClass() != obj.getClass()) {
			return false;
		}
		
		ConjunctionSymbolClass other = (ConjunctionSymbolClass) obj;
		
		return Objects.equal(conjunctedSymbolClasses, other.conjunctedSymbolClasses);
	}

	private class ConjunctionState implements SymbolClass.State {
		
		private final ImmutableSet<? extends SymbolClass.State> conjunctedStates;
		private final transient int hashCode;

		public ConjunctionState(ImmutableSet<? extends State> conjunctedStates) {
			this.conjunctedStates = Preconditions.checkNotNull(conjunctedStates, "Conjuncted states must not be null");
			this.hashCode = HashCodes.classSpecificHashCode(ConjunctionState.class, conjunctedStates);
		}

		@Override
		public int hashCode() {
			return hashCode;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			
			if (obj == null) {
				return false;
			}
			
			if (getClass() != obj.getClass()) {
				return false;
			}
			
			ConjunctionState other = (ConjunctionState) obj;
			
			return Objects.equal(getConjunctionSymbolClass(), other.getConjunctionSymbolClass())
			    && Objects.equal(conjunctedStates, other.conjunctedStates);
		}

		private ConjunctionSymbolClass getConjunctionSymbolClass() {
			return ConjunctionSymbolClass.this;
		}

	}

	@Override
	public <I, O> O accept(SymbolClassVisitor<I, O> visitor, I input) {
		return visitor.visitConjunctionSymbolClass(this, input);
	}

}
